Unity动作表情工具(编辑器模式下一边播动作一边播表情)

您所在的位置:网站首页 unity动作编辑器 插件 Unity动作表情工具(编辑器模式下一边播动作一边播表情)

Unity动作表情工具(编辑器模式下一边播动作一边播表情)

2024-07-13 15:37| 来源: 网络整理| 查看: 265

一直忙于学习技术和工作好久没写博客

这次分享一下我写的一个动作表情工具

先说一下需求:美术把一帧帧表情图导出来,一张张排好序号,然后放到编辑器里面打开一个工具界面可以选动作,同时切换对应的表情,在编辑器模式下播放动作和表情,还可以调整一下表情,最后可以保存数据放到游戏项目里面用

其实我是不喜欢这种,因为不共用,我喜欢的是左眼右眼嘴巴分开mesh,这样子每个部分复用率高,可以避免内存问题。不过分开3个mesh的话调表情的工作量会增大

还有其他实现方法例如使用blendshape,可以参考unity chan的demo

这里面涉及几个工具

模型prefab生成工具,包括animatorController(就是美术给的模型fbx和动作fbx)表情图处理工具,包括几个表情合在一张大图里面(例如1-1,2-1,2-2,3-1,这里面一张图时间为0.2秒,前面序号相同的表示同一个表情,所以表情2时间为0.2*2,这样子要合成的图片就是1,2,3三张图片合成一张,同时把对应的数据导出来)编辑器动作表情播放工具(表情处理工具导出一份数据出来的,打开这个工具直接读取数据,里面可以调整单个表情时间,添加表情,删除表情等等功能) 先给个大体流程看看 1.模型prefab生成工具

2.表情图处理

(1)原始美术给的表情图

(2)第一次处理表情,把表情筛选出来,生成这个动作表情数据

(3)第二次处理把所有的动作表情数据合并到总的表情数据里面,把筛选出来的图片合成大图

3.表情动作播放工具

 

规范

工具一般都要有规律,所以有些东西必须规范好

首先我这里规定好目录,在assets下创建Art文件夹,然后创建一个模型名字的文件夹

例如:

资源根目录:Assets/Art/模型名/放模型资源目录:Assets/Art/模型名/Model/表情资源根目录:Assets/Art/模型名/Expression/动作表情目录:Assets/Art/模型名/Expression/动作名/放未处理美术表情资源目录:Assets/Art/模型名/Expression/动作名/normal处理过美术表情资源目录(动态创建):Assets/Art/模型名/Expression/动作名/deal合成好的图片资源和表情数据(动态创建):Assets/Art/模型名/Result/这个模型的所有表情数据(动态创建):Assets/Art/模型名/Result/模型名_Express.txt合成的表情图的材质球路径:Assets/Art/ExpressionMaterial/

Ps:表情数据用json为了方便查看,我一般用用protobuf导出数据,因为protobuf比json速度快

 

 

这里我根据三个工具,分三个部分讲解

 

第一部分:模型animatorController,prefab生成工具 1.工具使用

(1)拿到美术给的资源,模型文件命名:模型名@model,动作命名:模型名@动作名

(2)选中model文件夹,右键处理模型,这里会自动生成模型名为名字的预设,还有一个挂上动画片段的animator Controller

2.工具讲解

先贴个代码,里面都有注释,我这里说一说流程

根据右键点击处理模型,获取选中的文件夹根据选中的文件夹,首先在这个文件夹创建一个animatorController

       使用的接口:AnimatorController.CreateAnimatorControllerAtPath

根据选中的文件夹,遍历所有.fbx文件或者.anim文件,以动作名创建一个状态加入animatorController第一个layer.stateMachine,然后这个状态的motion赋值动画片段最后把模型实例到场景,然后赋值animator的animatorController,最后把场景这个模型保存为预设,把场景的模型删除

       使用的接口:PrefabUtility.CreatePrefab

using System; using UnityEngine; using System.Collections; using System.IO; using System.Linq; using UnityEditor; using UnityEditor.Animations; using System.Drawing; using System.Collections.Generic; using System.Web; /// /// 动作控制器生成工具 /// public class AnimatorTool : MonoBehaviour { [MenuItem("Assets/处理模型", false)] static void DealAnimator() { //获取选中的目录路径 UnityEngine.Object[] arr = Selection.GetFiltered(typeof(UnityEngine.Object), SelectionMode.Assets); string assetPath = AssetDatabase.GetAssetPath(arr[0]); string fullPath = EditorTool.GetFullAssetPath(assetPath); DirectoryInfo info = new DirectoryInfo(fullPath); if (info.Name != "Model") { return; } string folderName = info.Parent.Name; // 创建animationController文件 AnimatorController aController = AnimatorController.CreateAnimatorControllerAtPath(string.Format("{0}/animation.controller", assetPath)); // 得到其layer var layer = aController.layers[0]; // 绑定动画文件 AddStateTranstion(string.Format("{0}", assetPath), layer); // 创建预设 GameObject go = LoadFbx(folderName, assetPath); if (null != go) { PrefabUtility.CreatePrefab(string.Format("{0}/{1}.prefab", assetPath, folderName), go); DestroyImmediate(go); } } /// /// 添加动画状态机状态 /// /// /// private static void AddStateTranstion(string path, AnimatorControllerLayer layer) { string[] paths = Directory.GetFiles(path, "*.fbx", SearchOption.AllDirectories); for (int i = 0; i < paths.Length; i++) { string temp = paths[i].Replace('\\', '/'); temp = temp.Substring(path.IndexOf("Assets/")); AnimatorStateMachine sm = layer.stateMachine; // 根据动画文件读取它的AnimationClip对象 var datas = AssetDatabase.LoadAllAssetsAtPath(temp); if (datas.Length == 0) { return; } // 遍历模型中包含的动画片段,将其加入状态机中 foreach (var data in datas) { if (!(data is AnimationClip)) continue; var newClip = data as AnimationClip; if (newClip.name.StartsWith("__")) continue; // 取出动画名字,添加到state里面 var state = sm.AddState(newClip.name); state.motion = newClip; } } //如果动画有处理过把fbx删掉只剩anim文件,就走这里 string[] ainPaths = Directory.GetFiles(path, "*.anim", SearchOption.AllDirectories); for (int i = 0; i < ainPaths.Length; i++) { string temp = ainPaths[i].Replace('\\', '/'); temp = temp.Substring(temp.IndexOf("Assets/")); AnimationClip clip = AssetDatabase.LoadAssetAtPath(temp); AnimatorStateMachine sm = layer.stateMachine; var state = sm.AddState(clip.name); state.motion = clip; } } /// /// 生成带动画控制器的对象 /// /// /// public static GameObject LoadFbx(string name, string assetPath) { UnityEngine.Object objr = AssetDatabase.LoadAssetAtPath(assetPath + "/" + name + "@model.FBX"); if (null == objr) { return null; } var obj = Instantiate(objr) as GameObject; obj.GetComponent().runtimeAnimatorController = AssetDatabase.LoadAssetAtPath(assetPath + "/animation.controller"); return obj; } }

 

  第二部分表情图处理工具 1.数据类 OneExpressionsData:单个”表情数据“,数据包括:使用那个贴图,这个贴图对应的材质球偏移位置,表情停留时间ExpressionsData:单个“动作表情数据”,数据包括:”表情数据“列表,动作名(用于区分表情)Animator2Expression:”所有动作表情数据“,数据包括:“动作表情数据”列表,uv名字(用于查找render) /// /// 所有动作表情数据 /// public class Animator2Expression { /// /// uv名字 /// public string UVName; /// /// 所有动作表情数据 /// public List AnimatorExpressionList = new List(); public int row; public int column; } /// /// 单个动作表情数据 /// public class ExpressionsData { /// /// 动作名 /// public string animationName; /// /// 所有表情数据 /// public List list = new List(); public bool AddTime(int index) { for (int i = 0; i < list.Count; i++) { if (index == list[i].index) { list[i].waitTime += 0.2d; System.Math.Round(list[i].waitTime, 3); return false; } } OneExpressionsData temp = new OneExpressionsData(index, System.Math.Round(0.2d, 3), GameDef.ExpressionRow, GameDef.ExpressionColumn); list.Add(temp); return true; } } /// /// 单帧表情数据 /// public class OneExpressionsData { /// /// 使用的图片名(用于读取材质球) /// public string UseImageName; /// /// 索引用于生成图片用 /// public int index; /// /// 表情等待时间 /// public double waitTime; /// /// 材质球截取x大小 /// public double TilingX; /// /// 材质球截取y大小 /// public double TilingY; /// /// 材质球x偏移 /// public double OffestX; /// /// 材质球y偏移 /// public double OffestY; public OneExpressionsData() { } public OneExpressionsData(int index, double time, float RowNum, float ColumnNum) { this.index = index; waitTime = time; TilingX = System.Math.Round(1.0d / ColumnNum, 3); TilingY = System.Math.Round(1.0d / RowNum, 3); } /// /// 根据所在图片索引计算位置信息 /// /// public void SetImageIndex(int ImageIndex) { this.index = ImageIndex; int ColumnIndex = ImageIndex / GameDef.ExpressionColumn; int RowIndex = ImageIndex % GameDef.ExpressionRow; SetIndexPos(RowIndex, ColumnIndex); } /// /// 设置所用的表情图 /// /// public void SetUseImageName(string name) { UseImageName = name; } /// /// 计算材质球位置 /// /// /// public void SetIndexPos(int RowIndex, int ColumnIndex) { OffestX = System.Math.Round(ColumnIndex * TilingX, 3); OffestY = System.Math.Round(-TilingY * (RowIndex + 1), 3); } } 2.图片处理工具使用

(1)现在以模型CZ-75,动作ShowTouchBody为例,把这些资源放到Assets/Art/CZ-75/Expression/ShowTouchBody/normal路径下

PS:资源放的路径可以看上面的规范

(2)选中Assets/Art/CZ-75/Expression/ShowTouchBody文件夹,然后右键->处理表情,如下图

(3)处理流程

筛选图片根据筛选出来的图片合成大图,把对应的表情数据导出来ExpressionsData把这个动作数据整合到Animator2Expression

(4)处理完毕之后

Assets/Art/CZ-75/Expression/ShowTouchBody/deal/这里面是筛选出来的图片和该动作的表情数据(ExpressionsData)

Assets/Art/CZ-75/Result/这里面是合成的图片和所有动作表情数据(Animator2Expression)

游戏里面只用到result里面的文件

PS:可以Assets/Art/CZ-75/选中文件夹右键->整个所有表情,把没有处理的表情全部处理(deal文件夹没有info.txt认为没有处理)

3工具讲解

(1)筛选图片和处理表情数据

我合成图片的索引从左上角开始,先从上到下,在左到右,

然后根据索引计算材质球偏移位置

OneExpressionsData数据类部分代码

public OneExpressionsData(int index, double time, float RowNum, float ColumnNum) { this.index = index; waitTime = time; TilingX = System.Math.Round(1.0d / ColumnNum, 3); TilingY = System.Math.Round(1.0d / RowNum, 3); } /// /// 计算材质球位置 /// /// /// public void SetIndexPos(int RowIndex, int ColumnIndex) { OffestX = System.Math.Round(ColumnIndex * TilingX, 3); OffestY = System.Math.Round(-TilingY * (RowIndex + 1), 3); }

该动作的表情数据处理,都是遍历文件夹里面图片

(例如1-1,2-1,2-2,3-1,这里面一张图时间为0.2秒,前面序号相同的表示同一个表情,所以表情2时间为0.2*2,这样子要合成的图片就是1,2,3三张图片合成一张)

/// /// 单独处理一个文件夹图片 /// /// /// static void DealOneAnimatorExpression(string fullPath, bool updateRootData = false) { DirectoryInfo mDirectoryInfo = new DirectoryInfo(fullPath); DirectoryInfo mRootDirctoryInfo = mDirectoryInfo.Parent.Parent; if (mDirectoryInfo.Parent.Name != "Expression") { return; } string expressionName = mRootDirctoryInfo.Name + mDirectoryInfo.Name; //合成图片文件夹 string outPutPath = mRootDirctoryInfo.ToString() + "/Result/"; string[] paths = Directory.GetFiles(fullPath + "/normal/", "*.png", SearchOption.AllDirectories); string dirPath = fullPath + "/deal/"; EditorTool.DeleteDirectory(dirPath); EditorTool.InitDirectory(dirPath); EditorTool.InitDirectory(outPutPath); List ppp = new List(paths); ppp.Sort((a, b) => { string ac = Path.GetFileName(a); string bc = Path.GetFileName(b); return int.Parse(ac.Split('-')[0]).CompareTo(int.Parse(bc.Split('-')[0])); }); ExpressionsData data = new ExpressionsData(); //动作名字以文件夹命名 data.animationName = mDirectoryInfo.Name; //遍历图片设置相同图片时间 for (int i = 0; i < ppp.Count; i++) { string ac = Path.GetFileName(ppp[i]); int tempIndex = int.Parse(ac.Split('-')[0]); if (data.AddTime(tempIndex)) { //把相同图片的一张图片放到deal文件夹 File.Copy(ppp[i], dirPath + tempIndex.ToString() + ".png", true); } } //重新设置图片索引 int lie = -1; int MergeImageIndex = -1; //遍历“动作表情”里面所有“表情数据” for (int i = 0; i < data.list.Count; i++) { OneExpressionsData mOneExpressionsData = data.list[i]; if ((i) % GameDef.ExpressionColumn == 0) { lie++; } if (i % GameDef.ImageNum == 0) { MergeImageIndex++; } //重新设置所有 mOneExpressionsData.index = i; //设置使用的图片(合成之后的) mOneExpressionsData.SetUseImageName(expressionName + MergeImageIndex); //计算材质球偏移位置 mOneExpressionsData.SetIndexPos(i % GameDef.ExpressionRow, lie % GameDef.ExpressionColumn); } string s = JsonMapper.ToJson(data); //把动作表情数据导出json到deal文件夹 EditorTool.SaveJosnFile(s, dirPath + "Info.txt"); AssetDatabase.Refresh(); //合成图片 MergeImage(dirPath, outPutPath, expressionName); if (updateRootData) { ConformData(mRootDirctoryInfo.ToString()); } }

(2)图片合成我们需要用到将System.Drawing引入Unity项目中

在Unity的安装路径中找到System.Drawing.dll,将其复制到我们的项目文件夹System.Drawing.dll的具体位置:%Unity根目录%\Editor\Data\Mono\lib\mono\2.0\System.Drawing.dll

(3)多张小图合成一张大图工具代码

/** * Author: YinPeiQuan **/ using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Web; /// /// 合成的图片顺序 /// public enum SortType { /// /// 左上角开始,从左到右,从上到下 /// width, /// /// 左上角开始,从上到下,从左到右 /// height, } public class ImageMergeHelper { /// /// 将多张图片拼接合并成一张指定大小的图片,各图像进行顺序排列 /// /// 新图像的高度 /// 新图像的宽度 /// 图像间距 /// 无图片时显示的文字,为空默认为:暂无图片 /// 图像数组 /// public static Image ImgMerge(int height, int width, int bw, SortType mtype , params Image[] imgs) { Image ret = new System.Drawing.Bitmap(width, height); Graphics g = Graphics.FromImage(ret); //这里设置透明底 g.Clear(Color.Empty); //新图像组合的图像个数 int cnt = GameDef.ExpressionRow * GameDef.ExpressionColumn; imgs = imgs.Take(cnt).ToArray(); //求新列表维数 int rat = Convert.ToInt16(Math.Sqrt(cnt)); if (rat > 0) { //图片宽高度不能小于2像素 if ((rat + 1) * bw + 2 * rat > width) bw = (width - 2 * rat) / (rat + 1); int th = (height - 2 * rat) / (rat + 1); if (th < bw) { //相对高度计算出来的间距,取小不取大,这样图像宽度显示更大一些 bw = th; } if (bw


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3